/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.codegen.core;

import com.querydsl.codegen.DefaultEntitySerializer;
import com.querydsl.codegen.EntityType;
import com.querydsl.codegen.Property;
import com.querydsl.codegen.SerializerConfig;
import com.querydsl.codegen.TypeMappings;
import com.querydsl.codegen.utils.CodeWriter;
import com.querydsl.codegen.utils.Symbols;
import com.querydsl.codegen.utils.model.ClassType;
import com.querydsl.codegen.utils.model.Parameter;
import com.querydsl.codegen.utils.model.SimpleType;
import com.querydsl.codegen.utils.model.Type;
import com.querydsl.codegen.utils.model.TypeCategory;
import com.querydsl.codegen.utils.model.Types;
import com.querydsl.sql.ColumnMetadata;
import com.querydsl.sql.ForeignKey;
import com.querydsl.sql.PrimaryKey;
import com.querydsl.sql.codegen.NamingStrategy;
import com.querydsl.sql.codegen.SQLCodegenModule;
import com.querydsl.sql.codegen.support.ForeignKeyData;
import com.querydsl.sql.codegen.support.InverseForeignKeyData;
import com.querydsl.sql.codegen.support.KeyData;
import com.querydsl.sql.codegen.support.PrimaryKeyData;
import net.apexes.commons.lang.Enume;
import net.apexes.commons.lang.Strings;
import net.apexes.commons.querydsl.Index;
import net.apexes.commons.querydsl.info.IndexColumn;
import net.apexes.commons.querydsl.sql.TablePathBase;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * 
 * @see com.querydsl.sql.codegen.MetaDataSerializer
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 *
 */
public class MetadataSerializer extends DefaultEntitySerializer {

    private static final String PK_VARIABLE = "pk";
    private static final String FK_VARIABLE = "fk";
    private static final String IFK_VARIABLE = "ifk";
    private static final String IX_VARIABLE = "ix";
    private static final Map<Integer, String> TYPE_CONSTANTS = new HashMap<>();
    static {
        try {
            for (Field field : java.sql.Types.class.getDeclaredFields()) {
                if (field.getType().equals(Integer.TYPE)) {
                    TYPE_CONSTANTS.put(field.getInt(null), field.getName());
                }
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    protected final NamingStrategy namingStrategy;

    protected final boolean innerClassesForKeys;

    private final Set<String> imports;

    private final Comparator<Property> columnComparator;

    private final Class<?> superClass;

    @Inject
    public MetadataSerializer(
            TypeMappings typeMappings,
            NamingStrategy namingStrategy,
            @Named(SQLCodegenModule.INNER_CLASSES_FOR_KEYS) boolean innerClassesForKeys,
            @Named(SQLCodegenModule.IMPORTS) Set<String> imports,
            @Named(SQLCodegenModule.COLUMN_COMPARATOR) Comparator<Property> columnComparator,
            @Named(SQLCodegenModule.ENTITYPATH_TYPE) Class<?> superClass) {
        super(typeMappings, Collections.emptyList());
        this.namingStrategy = namingStrategy;
        this.innerClassesForKeys = innerClassesForKeys;
        this.imports = new TreeSet<>(imports);
        this.columnComparator = columnComparator;
        if (!TablePathBase.class.isAssignableFrom(superClass)) {
            throw new RuntimeException("The entityPathType must extends " + TablePathBase.class + " : " + superClass);
        }
        this.superClass = superClass;
    }

    @Override
    protected void intro(EntityType model, SerializerConfig config,
                         CodeWriter writer) throws IOException {
        introPackage(writer, model);
        introImports(writer, config, model);

        introJavadoc(writer, model);
        introClassHeader(writer, model);

        introFactoryMethods(writer, model);
        introInits(writer, model);
        if (config.createDefaultVariable()) {
            introDefaultInstance(writer, model, config.defaultVariableName());
        }
        if (model.getSuperType() != null && model.getSuperType().getEntityType() != null) {
            introSuper(writer, model);
        }
    }

    @Override
    protected void constructorsForVariables(CodeWriter writer, EntityType model) throws IOException {
        super.constructorsForVariables(writer, model);

        String localName = writer.getRawName(model);
        String genericName = writer.getGenericName(true, model);

        if (!localName.equals(genericName)) {
            writer.suppressWarnings("all");
        }
        writer.beginConstructor(new Parameter("variable", Types.STRING),
                                new Parameter("schema", Types.STRING),
                                new Parameter("table", Types.STRING));
        writer.line(Symbols.SUPER,"(", writer.getClassConstant(localName) + Symbols.COMMA
                + "forVariable(variable), schema, table);");
        constructorContent(writer, model);
        writer.end();
    }

    @Override
    protected void constructorContent(CodeWriter writer, EntityType model) throws IOException {
        writer.line("addMetadata();");
    }

    @Override
    protected void introClassHeader(CodeWriter writer, EntityType model) throws IOException {
        Type queryType = typeMappings.getPathType(model, model, true);

        writer.line("@Generated(\"", getClass().getName(), "\")");

        TypeCategory category = model.getOriginalCategory();
        // serialize annotations only, if no bean types are used
        if (model.equals(queryType)) {
            for (Annotation annotation : model.getAnnotations()) {
                writer.annotation(annotation);
            }
        }
        writer.beginClass(queryType, new ClassType(category, superClass, model));
        writer.privateStaticFinal(Types.LONG_P, "serialVersionUID", String.valueOf(model.hashCode()));
    }

    @Override
    protected String getAdditionalConstructorParameter(EntityType model) {
        StringBuilder builder = new StringBuilder();
        if (model.getData().containsKey("schema")) {
            builder.append(", \"").append(model.getData().get("schema")).append("\"");
        } else {
            builder.append(", null");
        }
        builder.append(", \"").append(model.getData().get("table")).append("\"");
        return builder.toString();
    }

    @Override
    protected void introDefaultInstance(CodeWriter writer, EntityType entityType, String defaultName) throws IOException {
        String variableName = !defaultName.isEmpty() ? defaultName : namingStrategy.getDefaultVariableName(entityType);
        String alias = namingStrategy.getDefaultAlias(entityType);
        Type queryType = typeMappings.getPathType(entityType, entityType, true);
        writer.publicStaticFinal(queryType, variableName, Symbols.NEW + queryType.getSimpleName() + "(\"" + alias + "\")");
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void introImports(CodeWriter writer, SerializerConfig config, EntityType model) throws IOException {
        OrderableImportCodeWriter codeWriter = new OrderableImportCodeWriter(writer);

        super.introImports(codeWriter, config, model);

        Set<String> importClasses = new HashSet<>();
        importClasses.add(superClass.getName());
        importClasses.add(ColumnMetadata.class.getName());
        importClasses.add(java.sql.Types.class.getName());

        Collection<ForeignKeyData> foreignKeys = (Collection<ForeignKeyData>)
                model.getData().get(ForeignKeyData.class);
        Collection<InverseForeignKeyData> inverseForeignKeys = (Collection<InverseForeignKeyData>)
                model.getData().get(InverseForeignKeyData.class);
        Collection<IndexData> indexs = (Collection<IndexData>) model.getData().get(IndexData.class);
        boolean addJavaUtilImport = false;
        if (innerClassesForKeys) {
            importClasses.add(PrimaryKey.class.getName());
        }
        if (innerClassesForKeys && foreignKeys != null) {
            for (ForeignKeyData keyData : foreignKeys) {
                importClasses.add(keyData.getType().getFullName());
                if (keyData.getForeignColumns().size() > 1) {
                    addJavaUtilImport = true;
                }
            }
            importClasses.add(ForeignKey.class.getName());
        }
        if (innerClassesForKeys && inverseForeignKeys != null) {
            for (InverseForeignKeyData keyData : inverseForeignKeys) {
                importClasses.add(keyData.getType().getFullName());
                if (keyData.getForeignColumns().size() > 1) {
                    addJavaUtilImport = true;
                }
            }
            importClasses.add(ForeignKey.class.getName());
        }
        if (innerClassesForKeys && indexs != null) {
            importClasses.add(Index.class.getName());
        }
        if (addJavaUtilImport) {
            writer.importPackages(List.class.getPackage().getName());
        }

        codeWriter.importClasses(importClasses.toArray(new String[0]));
        writeUserImports(codeWriter);

        codeWriter.writeImport();
    }

    protected void writeUserImports(CodeWriter writer) throws IOException {
        Set<String> packages = new HashSet<>();
        Set<String> classes = new HashSet<>();

        for (String javaImport : imports) {
            //true if the character next to the dot is an upper case or if no dot is found (-1+1=0) the first character
            boolean isClass = Character.isUpperCase(javaImport.charAt(javaImport.lastIndexOf(".") + 1));
            if (isClass) {
                classes.add(javaImport);
            } else {
                packages.add(javaImport);
            }
        }

        String[] marker = new String[]{};
        if (!packages.isEmpty()) {
            writer.importPackages(packages.toArray(marker));
        }
        if (!classes.isEmpty()) {
            writer.importClasses(classes.toArray(marker));
        }
    }

    @Override
    protected void outro(EntityType model, CodeWriter writer) throws IOException {
        writer.beginPublicMethod(Types.VOID,"addMetadata");
        List<Property> properties = new ArrayList<>(model.getProperties());
        if (columnComparator != null) {
            properties.sort(columnComparator);
        }
        for (Property property : properties) {
            String name = property.getEscapedName();
            ColumnMetadata metadata = (ColumnMetadata) property.getData().get("COLUMN");
            StringBuilder columnMeta = new StringBuilder();
            columnMeta.append("ColumnMetadata");
            columnMeta.append(".named(\"").append(metadata.getName()).append("\")");
            columnMeta.append(".withIndex(").append(metadata.getIndex()).append(")");
            if (metadata.hasJdbcType()) {
                String type = String.valueOf(metadata.getJdbcType());
                if (TYPE_CONSTANTS.containsKey(metadata.getJdbcType())) {
                    type = "Types." + TYPE_CONSTANTS.get(metadata.getJdbcType());
                }
                columnMeta.append(".ofType(").append(type).append(")");
            }
            if (metadata.hasSize()) {
                columnMeta.append(".withSize(").append(metadata.getSize()).append(")");
            }
            if (metadata.getDigits() > 0) {
                columnMeta.append(".withDigits(").append(metadata.getDigits()).append(")");
            }
            if (!metadata.isNullable()) {
                columnMeta.append(".notNull()");
            }
            
            String defaultValue = (String) property.getData().get("COLUMN_DEF");
            if (defaultValue != null) {
                writer.line("addMetadata(", name, ", ", columnMeta.toString(), ", \"", defaultValue, "\");");
            } else {
                writer.line("addMetadata(", name, ", ", columnMeta.toString(), ");");
            }
        }
        writer.end();

        addNewInstance(model, writer);
        addGetFieldValue(model, properties, writer);
        addSetFieldValue(model, properties, writer);

        super.outro(model, writer);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void serializeProperties(EntityType model,  SerializerConfig config, CodeWriter writer) throws IOException {
        super.serializeProperties(model, config, writer);

        // primary keys
        PrimaryKeyData primaryKey = (PrimaryKeyData) model.getData().get(PrimaryKeyData.class);
        if (primaryKey != null) {
            String fieldName = namingStrategy.getPropertyNameForPrimaryKey(primaryKey.getName(), model);
            StringBuilder value = new StringBuilder("createPrimaryKey(");
            boolean first = true;
            for (String column : primaryKey.getColumns()) {
                if (first) {
                    first = false;
                } else {
                    value.append(", ");
                }
                value.append(namingStrategy.getPropertyName(column, model));
            }
            value.append(")");

            Type type = new ClassType(PrimaryKey.class, model);
            Type primaryKeysType = new SimpleType("PrimaryKeys");
            writer.beginClass(primaryKeysType);
            writer.publicFinal(type, fieldName, value.toString());
            writer.end();
            writer.publicFinal(primaryKeysType, PK_VARIABLE, "new " + primaryKeysType.getSimpleName() + "()");
        }

        if (innerClassesForKeys) {
            Collection<ForeignKeyData> foreignKeys = (Collection<ForeignKeyData>) model.getData().get(ForeignKeyData.class);
            Collection<InverseForeignKeyData> inverseForeignKeys = (Collection<InverseForeignKeyData>) model.getData().get(InverseForeignKeyData.class);
            Collection<IndexData> indexs = (Collection<IndexData>) model.getData().get(IndexData.class);

            Type foreignKeysType = new SimpleType("ForeignKeys");
            Type indexsType = new SimpleType("Indexs");

            // foreign keys
            if (foreignKeys != null) {
                writer.beginClass(foreignKeysType);
                serializeForeignKeys(model, writer, foreignKeys, false);
                writer.end();
                writer.publicFinal(foreignKeysType, FK_VARIABLE, "new " + foreignKeysType.getSimpleName() + "()");
            }

            // inverse foreign keys
            Type inverseForeignKeysType = new SimpleType("InverseForeignKeys");
            if (inverseForeignKeys != null) {
                writer.beginClass(inverseForeignKeysType);
                serializeForeignKeys(model, writer, inverseForeignKeys, true);
                writer.end();
                writer.publicFinal(inverseForeignKeysType, IFK_VARIABLE, "new " + inverseForeignKeysType.getSimpleName() + "()");
            }

            // indexs
            if (indexs != null) {
                writer.beginClass(indexsType);
                serializeIndexs(model, writer, indexs);
                writer.end();
                writer.publicFinal(indexsType, IX_VARIABLE, "new " + indexsType.getSimpleName() + "()");
            }
        }
    }
    
    @Override
    protected void serialize(EntityType model, Property property, Type type, CodeWriter writer,
            String factoryMethod, String... args) throws IOException {
        Field field = CodegenUtils.findAccessibleField(superClass, property.getName());
        if (field == null) {
            super.serialize(model, property, type, writer, factoryMethod, args);
        }
    }

    @Override
    protected void customField(EntityType model, Property field, SerializerConfig config,
            CodeWriter writer) throws IOException {
        if (Enume.class.isAssignableFrom(field.getType().getJavaClass())) {
            Type queryType = typeMappings.getPathType(field.getType(), model, false);
            String localRawName = writer.getRawName(field.getType());
            serialize(model, field, queryType, writer, "createEnume",
                    writer.getClassConstant(localRawName));
        } else {
            Type queryType = typeMappings.getPathType(field.getType(), model, false);
            String localRawName = writer.getRawName(field.getType());
            serialize(model, field, queryType, writer, "create" + field.getType().getSimpleName(),
                    writer.getClassConstant(localRawName));
        }
    }

    protected void serializeForeignKeys(EntityType model, CodeWriter writer,
            Collection<? extends KeyData> foreignKeys, boolean inverse) throws IOException {
        for (KeyData foreignKey : foreignKeys) {
            String fieldName;
            if (inverse) {
                fieldName = namingStrategy.getPropertyNameForInverseForeignKey(foreignKey.getName(), model);
            } else {
                fieldName = namingStrategy.getPropertyNameForForeignKey(foreignKey.getName(), model);
            }

            StringBuilder value = new StringBuilder();
            if (inverse) {
                value.append("createInvForeignKey(");
            } else {
                value.append("createForeignKey(");
            }
            if (foreignKey.getForeignColumns().size() == 1) {
                value.append(namingStrategy.getPropertyName(foreignKey.getForeignColumns().get(0), model));
                if (!inverse) {
                    value.append(", \"").append(foreignKey.getTable()).append("\"");
                }
                value.append(", \"").append(foreignKey.getParentColumns().get(0)).append("\"");
            } else {
                StringBuilder local = new StringBuilder();
                StringBuilder foreign = new StringBuilder();
                for (int i = 0; i < foreignKey.getForeignColumns().size(); i++) {
                    if (i > 0) {
                        local.append(", ");
                        foreign.append(", ");
                    }
                    local.append(namingStrategy.getPropertyName(foreignKey.getForeignColumns().get(i), model));
                    foreign.append("\"").append(foreignKey.getParentColumns().get(i)).append("\"");
                }
                value.append("Arrays.asList(").append(local).append(")");
                if (!inverse) {
                    value.append(", \"").append(foreignKey.getTable()).append("\"");
                }
                value.append(", Arrays.asList(").append(foreign).append(")");
            }
            value.append(")");
            Type type = new ClassType(ForeignKey.class, foreignKey.getType());
            writer.publicFinal(type, fieldName, value.toString());
        }
    }
    
    protected void serializeIndexs(EntityType model, CodeWriter writer, Collection<IndexData> indexs) throws IOException {
        Type indexType = new ClassType(Index.class);
        for (IndexData index : indexs) {
            String indexName = index.getName();
            String fieldName = namingStrategy.getPropertyNameForPrimaryKey(indexName, model);
            StringBuilder value = new StringBuilder("index(\"").append(indexName).append("\")");
            if (index.isUnique()) {
                value.append(".unique()");
            }
            for (IndexColumn indexColumn : index.getColumns()) {
                String columnName = namingStrategy.getPropertyName(indexColumn.getColumnName(), model);
                value.append(".column(").append(columnName);
                if (indexColumn.isDesc()) {
                    value.append(", ").append(true);
                }
                value.append(")");
            }
            value.append(".build()");
            writer.publicFinal(indexType, fieldName, value.toString());
        }
    }

    protected void addNewInstance(EntityType model, CodeWriter writer) throws IOException {
        writer.annotation(Override.class);
        writer.beginPublicMethod(model, "newEntity");
        writer.line("return new ", model.getSimpleName(), "();");
        writer.end();
    }

    protected void addGetFieldValue(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        Parameter entityParam = new Parameter("entity", model);
        Parameter fieldNameParam = new Parameter("fieldName", Types.STRING);
        writer.annotation(Override.class);
        writer.beginPublicMethod(Types.OBJECT, "getFieldValue", entityParam, fieldNameParam);
        writer.line("switch (fieldName) {");
        for (Property property : properties) {
            String name = property.getName();
            writer.line("    case \"", name, "\":");
            writer.line("        return entity.get", Strings.capitalize(name), "();");
        }
        writer.line("    default:");
        writer.line("        return super.getFieldValue(entity, fieldName);");
        writer.line("}");
        writer.end();
    }

    protected void addSetFieldValue(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        Parameter entityParam = new Parameter("entity", model);
        Parameter fieldNameParam = new Parameter("fieldName", Types.STRING);
        Parameter valueParam = new Parameter("value", Types.OBJECT);
        writer.annotation(Override.class);
        writer.beginPublicMethod(Types.VOID, "setFieldValue", entityParam, fieldNameParam, valueParam);
        writer.line("switch (fieldName) {");
        for (Property property : properties) {
            String name = property.getName();
            String typeFullName = property.getType().getFullName();
            writer.line("    case \"", name, "\":");
            writer.line("        entity.set", Strings.capitalize(name), "((", typeFullName, ") value);");
            writer.line("        break;");
        }
        writer.line("    default:");
        writer.line("        super.setFieldValue(entity, fieldName, value);");
        writer.line("        break;");
        writer.line("}");
        writer.end();
    }

}
